from ..utils.config_reader import read_util
from ..utils.logger import logger
import requests, random, string, json, base64, pymysql
from bs4 import BeautifulSoup
from typing import Any, Dict, List
import re
import functools


# 自定义异常类
class CaptchaError(Exception):
    pass


class ServerUtils:

    def __init__(self):
        self.sessions = requests.Session()
        self.header = {'Cookie': read_util.read_data()['session_id']}
        # self.base_url = read_util.read_data()['接口域名地址']['后台测试地址']  # 替换为实际的基础 URL
        self.base_url = read_util.read_data()['接口域名地址']['后台预生产地址']
        # self.base_url = read_util.read_data()['接口域名地址']['后台生产地址']
        self.current_phone = None
        self.connection = None

    def get_request(self, url, params=None, log=True):
        """统一的 GET 请求方法"""
        url = self.base_url + url
        try:
            response = self.sessions.get(url, params=params, headers=self.header)
            self.log_api_request_response(url, params, response)
            return response.text
        except Exception as e:
            server_utils.handle_exception(e, f"接口{url}请求失败,参数{params}")

    def post_request(self, url, data=None, json_data=None):
        """统一的 POST 请求方法"""
        url = self.base_url + url
        try:
            # 如果json_data不为空，则将其转换为JSON格式，并设置Content-Type为application/json
            if json_data is not None:
                self.header['Content-Type'] = 'application/json'
                data = json.dumps(json_data)

            response = self.sessions.post(url, headers=self.header, data=data)
            if 'application/json' in response.headers.get('Content-Type'):
                self.log_api_request_response(url, data, response.json())
                return response
            else:
                self.log_api_request_response(url, data, response)
                return response.text
        except Exception as e:
            server_utils.handle_exception(e, f"接口{url}请求失败, 参数{data}")

    # def post_request(self, url, data=None, log=True):
    #     """统一的 POST 请求方法"""
    #     url = self.base_url + url
    #     try:
    #         response = self.sessions.post(url, headers=self.header, data=data)
    #         if 'application/json' in response.headers.get('Content-Type'):
    #             self.log_api_request_response(url, data, response.json())
    #             return response
    #         else:
    #             self.log_api_request_response(url, data, response)
    #             return response.text
    #     except Exception as e:
    #         server_utils.handle_exception(e, f"接口{url}请求失败, 参数{data}")

    # def get_current_session(self):
    #     """获取当前的用户会话"""
    #     current_phone = self.current_phone if hasattr(self, 'current_phone') else None
    #     return self.sessions.get(current_phone)

    @staticmethod
    def generate_random_string(rando_name="测试课程", length=6):
        # 从大写字母、小写字母和数字中随机选择字符
        characters = string.ascii_letters  # 包含所有大小写字母
        return rando_name + ''.join(random.choice(characters) for _ in range(length))

    @staticmethod
    def verification_code():
        """
        识别验证码并返回识别结果。
        """
        with open("captcha_image.jpeg", 'rb') as f:
            base64_data = base64.b64encode(f.read())
        b64 = base64_data.decode()

        data = {"username": "ayin", "password": "Qz6528608", "typeid": 1003, "image": b64}
        result = json.loads(requests.post("http://api.ttshitu.com/predict", json=data).text)

        if result['success']:
            return result["data"]["result"]
        else:
            return result["message"]

    def get_img_code(self, session):
        # 获取图片验证码二进制文本
        image_response = session.get(url="https://testadmin.xkb365.com/captcha")
        # 将图片数据写入文件
        with open("captcha_image.jpeg", 'wb') as file:
            file.write(image_response.content)
        # 生成验证码并返回
        return self.verification_code()

    def get_app_img_code(self, base64_data):
        # 去除数据URI前缀
        base64_data = base64_data.split(",")[1]  # 提取`base64`部分

        # 检查Base64数据是否符合规则，添加填充符号
        missing_padding = len(base64_data) % 4
        if missing_padding:
            base64_data += '=' * (4 - missing_padding)

        # 解码Base64数据
        image_data = base64.b64decode(base64_data)
        # 将解码后的数据写入文件
        with open("captcha_image.jpeg", 'wb') as file:
            file.write(image_data)
        # 生成验证码并返回
        return self.verification_code()

    @staticmethod
    def save_image_from_url(image_url, save_path):
        """
        从 URL 下载图片并保存为文件。

        :param image_url: 图片的 URL
        :param save_path: 保存图片的文件路径
        """
        # 发送 GET 请求获取图片数据
        response = requests.get(image_url)

        # 确保请求成功
        if response.status_code == 200:
            # 将图片数据写入文件
            with open(save_path, 'wb') as file:
                file.write(response.content)
        else:
            print(f"获取图片失败，状态码: {response.status_code}")

    # def login_background(self, login_phone, login_password):
    #     try:
    #         # 检查用户会话是否已存在，若不存在则创建一个新的 Session
    #         if login_phone not in self.sessions:
    #             self.sessions[login_phone] = requests.Session()
    #
    #         session = self.sessions[login_phone]
    #         self.current_phone = login_phone  # 记录当前用户手机号
    #
    #         # 使用统一的 GET 请求方法请求登录页面
    #         login_page_response = self.get_request(url="/loginpage.html")
    #
    #         # 获取当前会话的 cookies
    #         if login_page_response is not None:
    #             # 将登录页面的 cookies 存入当前会话
    #             session.cookies.update(login_page_response.cookies)
    #
    #         # 登录接口请求参数
    #         login_data = {
    #             "autoLogin": "2",
    #             "loginName": login_phone,
    #             "loginPassword": login_password,
    #             "kaptchaCode": self.get_img_code(session)  # 使用当前的 session 获取验证码
    #         }
    #         # 使用统一的 POST 请求方法进行登录
    #         response = self.post_request(url="/login.html", data=login_data)
    #
    #         if response is not None and response.status_code == 200:
    #             return session  # 返回当前的会话
    #         else:
    #             logger.error("登录请求失败，状态码：", response.status_code if response else "无响应")
    #             return None
    #     except Exception as e:
    #         server_utils.handle_exception(e, "后台登录失败")

    def execute_sql_function(self, sql):
        # 数据库连接配置
        config = {
            'host': read_util.read_data()['数据库']['host'],
            'user': read_util.read_data()['数据库']['user'],
            'password': read_util.read_data()['数据库']['password'],
            'port': read_util.read_data()['数据库']['port'],  # MySQL 默认端口
        }

        # 连接到数据库
        try:
            self.connection = pymysql.connect(**config)
            logger.info("成功连接到数据库")

            # 创建一个游标对象
            with self.connection.cursor() as cursor:
                # 执行 UPDATE 语句
                cursor.execute(sql)

                # 获取受影响的行数
                affected_rows = cursor.rowcount
                logger.info(f"受影响的行数: {affected_rows}")

                # 提交事务，如果你没有设置自动提交的话
                self.connection.commit()

        except pymysql.MySQLError as e:
            logger.info(f"连接数据库时发生错误: {e}")

        finally:
            # 关闭连接
            if self.connection:
                self.connection.close()
                logger.info("数据库连接已关闭")

    @staticmethod
    def get_button_value_from_response(html_content, string_name, button_name):
        # 解析 HTML
        soup = BeautifulSoup(html_content, 'html.parser')

        # 查找并提取按钮的 detail 参数值
        for button in soup.find_all('button'):
            if button.get_text(strip=True) == f'{string_name}':
                onclick_value = button['onclick']

                # 使用正则表达式提取 button_name 的参数值
                detail_value = re.search(rf"{button_name}\('([^']+)", onclick_value)  # 可选的第二个参数
                if detail_value:
                    # 提取第一个参数值
                    param1 = detail_value.group(1)  # 第一个参数
                    return param1

    @staticmethod
    def get_input_value_from_response(html_content, class_name, input_name):
        # 创建 BeautifulSoup 对象
        soup = BeautifulSoup(html_content, 'html.parser')

        # 查找 input 标签
        input_tag = soup.find('input', {'class': f'{class_name}'})

        # 获取 value 属性
        if input_tag:
            value = input_tag.get(f'{input_name}')
            return value
        else:
            logger.error("没有找到 input 标签")

    @staticmethod
    def raise_captcha_error(message):
        raise CaptchaError(message)

    @staticmethod
    def log_api_request_response(url, params, response):
        """
        记录API请求和响应的日志。

        :param url: 请求的接口地址
        :param params: 请求参数
        :param response: 请求响应
        """
        logger.info(f"请求地址--------{url}")
        logger.info(f"请求参数--------{params}")
        logger.info(f"请求响应--------{response}")
        logger.info("")

    @staticmethod
    def handle_exception(e, message):
        logger.error(message, exc_info=True)  # 记录日志
        raise CaptchaError(f"{message}: {e}") from e  # 抛出新的异常

    def handle_exceptions(self, error_message):
        def decorator(func):
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    self.handle_exception(e, error_message)

            return wrapper

        return decorator

    @staticmethod
    def get_value_from_path(data, path):
        """
        从数据中根据路径提取值。

        :param data: 包含数据的字典或列表
        :param path: 由键和索引组成的路径字符串，例如 'appData.partList.0.videoDataId'
        :return: 提取的值
        """
        part = None
        try:
            parts = path.split('.')
            value = data
            for part in parts:
                if isinstance(value, list):
                    # 如果当前值是列表，解析索引
                    index = int(part)
                    value = value[index]
                else:
                    # 如果当前值是字典，解析键
                    value = value[part]
            return value
        except KeyError:
            logger.error(f"KeyError: 缺少键 '{part}' 在路径 '{path}' 中")
            return None
        except IndexError:
            logger.error(f"IndexError: 列表索引 '{part}' 超出范围在路径 '{path}' 中")
            return None
        except ValueError:
            logger.error(f"ValueError: 无法将 '{part}' 转换为整数在路径 '{path}' 中")
            return None

    def assign_request_data(self, test_case, shared_data, data_mapping_config):
        for target_key, source_paths in data_mapping_config.items():
            if isinstance(source_paths, str):
                value = self.get_value_from_path(shared_data, source_paths)
                if value is not None:
                    test_case[target_key] = value
            elif isinstance(source_paths, list):
                if target_key not in test_case:
                    test_case[target_key] = []
                else:
                    # 确保 target_key 对应的是一个列表
                    if not isinstance(test_case[target_key], list):
                        test_case[target_key] = [test_case[target_key]]
                # 确保 test_case[target_key] 的长度足够
                while len(test_case[target_key]) < len(source_paths):
                    test_case[target_key].append({})
                for i, path_dict in enumerate(source_paths):
                    for key, path in path_dict.items():
                        value = self.get_value_from_path(shared_data, path)
                        if value is not None:
                            test_case[target_key][i][key] = value

    def generic_test_function(self, test_case, request_api, shared_data=None, need_assert=True, request_data_key='request_data', assert_data_key='expected_result'):
        """
        集成获取测试用例数据、接口调用api、断言数据一体的多功能函数

        :param test_case: 测试用例数据
        :param request_api: 接口请求api
        :param request_data_key: 测试用例数据中请求参数的键名，默认为'request_data'
        :param shared_data: 是否需要从夹具获取其他接口的返回值进行传参，需要则传入夹具，不需要则传None
        :param need_assert: 是否需要断言，默认True就是需要，不需要传False
        :param assert_data_key: 测试用例数据中接口断言的键名，默认为'expected_result'
        """
        logger.info(f"------{test_case['name']}------")

        # 从共享数据中获取所需字段，并映射到请求参数
        if shared_data is not None:
            if isinstance(shared_data, list):
                for data_dict in shared_data:
                    self.assign_request_data(test_case[request_data_key], data_dict, test_case['data_mapping_config'])
            else:
                self.assign_request_data(test_case[request_data_key], shared_data, test_case['data_mapping_config'])

        # logger.debug(shared_data)
        # logger.debug(test_case[request_data_key])

        # 调用相应的接口方法
        response = request_api(test_case[request_data_key])

        # 断言响应字段
        if need_assert:
            try:
                read_util.assert_response(test_case[assert_data_key], response)
            except KeyError as e:
                logger.error(f"当前{test_case['name']}测试用例中没有expected_result这个键")

        return response

    # 定义 find_keys_with_condition 函数，返回单个值
    def find_keys_with_condition(self, data, key, condition=None, return_first=True):
        """
        递归函数，用于在嵌套的字典或列表中查找符合条件的键值。
        :param data: 要查找的数据，可以是字典或列表。
        :param key: 要查找的键。
        :param condition: 可选的条件字典，用于进一步过滤匹配的键值。
        :param return_first: 是否在找到第一个匹配项时返回，默认为True。
        :return: 找到的值，如果没有找到则返回None。
        """
        try:
            key_value = None  # 初始化 key_value 为 None，用于存储找到的键值
            if isinstance(data, dict):  # 如果数据是字典
                for k, v in data.items():  # 遍历字典的键值对
                    if k == key:  # 如果当前键与目标键匹配
                        if condition is None or (
                                isinstance(condition, dict) and all(data.get(cond_key) == cond_value for cond_key, cond_value in condition.items())):
                            '''
                            检查条件是否满足：
                            1. 如果条件为 None，直接通过。
                            2. 如果条件是字典，则检查 data 中的每个条件键值对是否与 condition 中的键值对匹配。
                            '''
                            if return_first:  # 如果只需要返回第一个匹配项
                                return v  # 返回匹配的值
                            else:
                                key_value = v  # 否则将值存储到 key_value 中
                    else:
                        # 如果当前键不匹配，递归调用函数继续查找嵌套的值
                        result = self.find_keys_with_condition(v, key, condition, return_first)
                        if result is not None:  # 如果找到匹配项
                            return result if return_first else result or key_value  # 根据 return_first 决定返回值
            elif isinstance(data, list):  # 如果数据是列表
                for item in data:  # 遍历列表中的每个元素
                    result = self.find_keys_with_condition(item, key, condition, return_first)  # 递归调用函数
                    if result is not None:  # 如果找到匹配项
                        return result if return_first else result or key_value  # 根据 return_first 决定返回值
            return key_value  # 如果没有找到匹配项，返回 None
        except Exception as e:
            logger.error(f"find_keys_with_condition 方法发生错误: {e}")  # 捕获异常并记录日志
            return None  # 返回 None 表示发生错误

    @staticmethod
    def populate_data(shared_data: Dict[str, Any], source_info: List[Any], key_list: List[str]):
        """
        将 source_info 中的数据填充到 shared_data 字典中，使用 key_list 中的键名。

        :param shared_data: 目标字典，用于存储填充的数据。
        :param source_info: 包含要填充数据的列表。
        :param key_list: 包含目标键名的列表。
        :return: 无返回值，直接修改 shared_data 字典。
        """
        try:
            # 检查 source_info 和 key_list 的长度是否一致
            if len(source_info) != len(key_list):
                raise ValueError("source_info 和 key_list 的长度必须一致")

            # 使用 zip 函数将 key_list 和 source_info 配对
            for key, value in zip(key_list, source_info):
                # 将每一对键值填充到 shared_data 字典中
                shared_data[key] = value
        except Exception as e:
            # 捕获异常并记录错误日志
            logger.error(f"populate_data 方法发生错误: {e}")

    def filter_by_conditions(self, data, key, conditions=None):
        """
        根据多个条件从数据中筛选出符合条件的键值。

        :param data: 数据源，通常是一个字典或 JSON 对象
        :param key: 需要查找的键（如 'taskId'）
        :param conditions: 条件列表，每个元素是一个字典，表示一个条件
        :return: 符合条件的键值列表
        """
        if conditions is None:
            return [self.find_keys_with_condition(data, key)]
        else:
            return [
                self.find_keys_with_condition(data, key, condition=cond)
                for cond in conditions
            ]


server_utils = ServerUtils()
